package log

import (
	"fmt"
	aactor "github.com/asynkron/protoactor-go/actor"
	"log"
	"os"
	"path"
	"path/filepath"
	"strings"
	"sync"
	"time"

	sadefine "gitee.com/simplexyz/simplego/actor/define"
	satimer "gitee.com/simplexyz/simplego/actor/timer"
	sutil "gitee.com/simplexyz/simplego/util"
)

type file struct {
	*os.File
	w              *fileWriter
	level          Level
	path           string
	size           int64
	lastRotateTime time.Time
}

func createFile(writer *fileWriter, level Level) (*file, error) {
	f := &file{
		w:              writer,
		level:          level,
		lastRotateTime: time.Now(),
	}
	if f.level.SubName() != "" {
		f.path = filepath.Join(f.w.Dir, f.w.Name+"."+f.level.SubName()+".log")
	} else {
		f.path = filepath.Join(f.w.Dir, f.w.Name+".log")
	}
	var err error
	f.File, err = os.OpenFile(f.path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		return nil, err
	}
	info, err := os.Lstat(f.path)
	if err != nil {
		return nil, err
	}
	f.size = info.Size()
	return f, nil
}

func (f *file) write(record IRecord) bool {
	if f.level != LevelAll && f.level != record.Level() {
		return false
	}
	n, _ := f.File.Write(record.Bytes())
	f.size += int64(n)
	return true
}

func (f *file) rotate() {
	_ = f.Sync()
	_ = f.Close()

	oldPath := f.path
	newPath := f.path
	nowTime := time.Now()
	timestamp := nowTime.Format("2006-01-02-15-04-05")
	if nowTime.Sub(f.lastRotateTime) < time.Second {
		ms := nowTime.Format(".000")
		ms = strings.Replace(ms, ".", "-", 1)
		timestamp += ms
	}
	if f.level.SubName() != "" {
		newPath = filepath.Join(f.w.Dir, fmt.Sprintf("%s.%s.%s.log", f.w.Name, f.level.SubName(), timestamp))
	} else {
		newPath = filepath.Join(f.w.Dir, fmt.Sprintf("%s.%s.log", f.w.Name, timestamp))
	}
	_ = os.Rename(oldPath, newPath)

	f.File, _ = os.OpenFile(f.path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	f.size = 0
	f.lastRotateTime = nowTime
}

type FileOption struct {
	Name            string        // 文件名
	Dir             string        // 目录
	MaxSize         int64         // 单个文件大小上限
	MaxAge          time.Duration // 文件保存时长
	SeperatedLevels int32         // 单独写文件的等级
	CheckInterval   time.Duration // 检查间隔
}

type fileWriter struct {
	FileOption

	lastCheckTime time.Time // 上次检查时刻

	files []*file

	startedWg sync.WaitGroup

	pid *sadefine.PID

	startedOnce sync.Once

	timerMgr *satimer.Manager

	count sync.WaitGroup
}

func CreateFileWriter(option *FileOption) (IWriter, error) {
	if option == nil {
		option = &FileOption{
			SeperatedLevels: LevelError,
		}
	}

	w := &fileWriter{
		FileOption:    *option,
		lastCheckTime: time.Now(),
	}

	var err error

	if w.Name == "" {
		// 默认使用可执行文件名（不带扩展名）
		w.Name, err = sutil.GetProgramFileBaseName()
		if err != nil {
			return nil, fmt.Errorf("init file writer name fail, %w", err)
		}
	}
	if w.Dir == "" {
		// 默认保存在当前目录
		w.Dir = "."
	}
	if w.MaxSize <= 0 {
		// 默认单个文件最大30MB
		w.MaxSize = 30 * 1024 * 1024
	}
	if w.MaxAge <= 0 {
		// 默认保存30天
		w.MaxAge = 30 * 24 * time.Hour
	}
	if w.CheckInterval <= 0 {
		// 默认超时间隔30秒
		w.CheckInterval = 30 * time.Second
	}

	err = sutil.MkDir(w.Dir)
	if err != nil {
		return nil, fmt.Errorf("create directory fail, %w", err)
	}

	f, e := createFile(w, LevelAll)
	if e != nil {
		return nil, fmt.Errorf("create file fail, path=[%s], %w", f.path, e)
	}

	w.files = append(w.files, f)

	if w.SeperatedLevels > 0 {
		EachLevel(func(level Level) (continued bool) {
			if !level.IsIn(w.SeperatedLevels) {
				return true
			}
			f, e = createFile(w, level)
			if e != nil {
				err = fmt.Errorf("open f fail, path=[%s], %w", f.path, e)
				return false
			}
			w.files = append(w.files, f)
			return true
		})
		if err != nil {
			return nil, err
		}
	}

	w.startedWg.Add(1)

	props := aactor.PropsFromProducer(func() aactor.Actor { return w } /*, aactor.WithMailbox(aactor.Unbounded())*/)
	_, err = rootContext().SpawnNamed(props, w.Name)
	if err != nil {
		return nil, err
	}

	writers.Store(w.Name, w)

	w.startedWg.Wait()

	return w, nil
}

func MustCreateFileWriter(option *FileOption) IWriter {
	w, err := CreateFileWriter(option)
	if err != nil {
		panic(err)
	}
	return w
}

func (w *fileWriter) Write(record IRecord) {
	w.count.Add(1)
	rootContext().Send(w.pid, record)
}

func (w *fileWriter) Destroy() {
	rootContext().Poison(w.pid)
	w.count.Wait()
}

func (w *fileWriter) tryRotate(f *file) bool {
	if f.size >= w.MaxSize {
		f.rotate()
		return true
	}
	if !sutil.IsSameDay(time.Now(), w.lastCheckTime) {
		f.rotate()
		return true
	}
	return false
}

func (w *fileWriter) onTimeout(ctx sadefine.Context, id satimer.ID, tag satimer.Tag) {
	//log.Println("onTimeout")

	// 滚转
	for _, f := range w.files {
		w.tryRotate(f)
	}

	now := time.Now()

	var needBackupFilePaths []string

	// 将超时文件归档，然后删除
	//log.Print("remove timeout file begin")
	_ = sutil.Walk(w.Dir, "log", nil, nil, func(path string, info os.FileInfo, name string, isInBlackList bool) (continued bool) {
		//log.Printf("path=%s", path)

		for _, f := range w.files {
			if info.Name() == sutil.GetFileName(f.path) {
				//log.Printf("ignore file %s", path)
				return true
			}
		}

		if now.Sub(info.ModTime()) < w.MaxAge {
			return true
		}

		needBackupFilePaths = append(needBackupFilePaths, path)
		log.Printf("find time out file %s", path)
		return true
	})

	if len(needBackupFilePaths) > 0 {
		timestamp := now.Format("2006-01-02-15-04-05")
		// 单独起一个协程执行来避免阻塞
		go func() {
			backupFilePath := path.Join(w.Dir, fmt.Sprintf("%s.%s.log.zip", w.Name, timestamp))
			backupFilePath, _ = filepath.Abs(backupFilePath)
			if err := sutil.Zip(needBackupFilePaths, backupFilePath); err != nil {
				log.Printf("backup log file to %s fail, %v", backupFilePath, err)
			} else {
				log.Printf("backup log file to %s success", backupFilePath)
			}
			for _, p := range needBackupFilePaths {
				_ = os.Remove(p)
			}
		}()
	}
	//log.Print("remove timeout file end")

	w.lastCheckTime = now
}

func (w *fileWriter) Receive(ctx sadefine.Context) {
	switch msg := ctx.Message().(type) {
	case IRecord:
		defer func() {
			w.count.Done()
			destroyRecord(msg)
		}()

		for _, f := range w.files {
			if f.write(msg) {
				w.tryRotate(f)
			}
		}

	case *satimer.MessageTimeout:
		defer satimer.DestroyMessageTimeout(msg)

		err := w.timerMgr.Trigger(ctx, msg.ID)
		if err != nil {
			log.Printf("[%s] trigger timer fail, id=%d, tag=%d, %s", w.pid.Id, msg.ID, msg.Tag, err)
			return
		}

	case *sadefine.Started:
		w.startedOnce.Do(func() {
			w.pid = ctx.Self().Clone()

			w.timerMgr = satimer.NewManager(rootContext(), w.pid)
			w.timerMgr.NewLoopTimer(w.CheckInterval, 0, w.onTimeout)

			w.startedWg.Done()
		})
	}
}
